package com.gzz.orchid.common.webmvc.support.repeatsubmit;


import com.gzz.orchid.common.core.authorization.UserContextHolder;
import com.gzz.orchid.common.core.exception.BizzException;
import com.gzz.orchid.common.core.util.CryptoUtil;
import com.gzz.orchid.common.webmvc.util.ReqUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;


/**
 * @author higmd
 * 定义一个切面类
 **/
@Slf4j
@Aspect
@Component
public class ReSubmitAspect {
    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 定义 @Pointcut注解表达式, 通过特定的规则来筛选连接点, 就是Pointcut，选中那几个你想要的方法
     * 在程序中主要体现为书写切入点表达式（通过通配、正则表达式）过滤出特定的一组 JointPoint连接点
     * 方式一：@annotation：当执行的方法上拥有指定的注解时生效（本博客采用这）
     * 方式二：execution：一般用于指定方法的执行
     */
    @Pointcut("@annotation(repeatSubmit)")
    public void pointCutNoRepeatSubmit(ReSubmit reSubmit) {

    }

    /**
     * 环绕通知, 围绕着方法执行
     *
     * @ Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
     * <p>
     * 方式一：单用 @Around("execution(* net.wnn.controller.*.*(..))")可以
     * 方式二：用@Pointcut和@Around联合注解也可以（本博客采用这个）
     * <p>
     * <p>
     * 两种方式
     * 方式一：加锁 固定时间内不能重复提交
     * <p>
     * 方式二：先请求获取token，这边再删除token,删除成功则是第一次提交
     */
    @Around("pointCutNoRepeatSubmit(repeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, ReSubmit reSubmit) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        long accountNo = UserContextHolder.currentUser().getUserId();
        //用于记录成功或者失败
        boolean res = false;
        //防重提交类型
        String type = reSubmit.limitType().name();
        if (type.equalsIgnoreCase(ReSubmit.Type.PARAM.name())) {
            //方式一，参数形式防重提交
            long lockTime = reSubmit.lockTime();
            String ipAddr = ReqUtil.getIpAddress();
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method method = methodSignature.getMethod();
            String className = method.getDeclaringClass().getName();
            String key = "order-server:repeat_submit:" + CryptoUtil.md5Encode(String.format("%s-%s-%s-%s", ipAddr, className, method, accountNo));
            //加锁
            // 这种也可以 本博客也介绍下redisson的使用
            // res  = redisTemplate.opsForValue().setIfAbsent(key, "1", lockTime, TimeUnit.SECONDS);
            //RLock lock = redissonClient.getLock(key);
            // 尝试加锁，最多等待0秒，上锁以后5秒自动解锁 [lockTime默认为5s, 可以自定义]
            //res = lock.tryLock(0,lockTime,TimeUnit.SECONDS);
        } else {
            //方式二，令牌形式防重提交
            String requestToken = request.getHeader("request-token");
            if (requestToken.isBlank()) {
                //throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL);
                throw new BizzException("订单TOKEN确认失败");
            }
            String key = String.format(ReSubmit.DEFAULT_TOKEN_KEY, accountNo, requestToken);
            /**
             * 提交表单的token key
             * 方式一：不用lua脚本获取再判断，之前是因为 key组成是 order:submit:accountNo, value是对应的token，所以需要先获取值，再判断
             * 方式二：可以直接key是 order:submit:accountNo:token,然后直接删除成功则完成
             */
            res = redisTemplate.delete(key);
        }
        if (!res) {
            log.error("请求重复提交");
            log.info("环绕通知中");
            return null;
        }
        log.info("环绕通知执行前");
        Object obj = joinPoint.proceed();
        log.info("环绕通知执行后");
        return obj;
    }
}